Skip to content

Commit b224c3f

Browse files
Rich-Harris7nikdummdidumm
authored
fix: coarse reactivity, alternative approach (#16100)
Make sure we track statically visible dependencies and untrack indirect dependencies Fixes #14351 --------- Co-authored-by: 7nik <[email protected]> Co-authored-by: Simon H <[email protected]>
1 parent d99d872 commit b224c3f

File tree

38 files changed

+392
-87
lines changed

38 files changed

+392
-87
lines changed

.changeset/popular-dancers-switch.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: use compiler-driven reactivity in legacy mode template expressions

packages/svelte/src/compiler/phases/1-parse/state/tag.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ function open(parser) {
6363
end: -1,
6464
test: read_expression(parser),
6565
consequent: create_fragment(),
66-
alternate: null
66+
alternate: null,
67+
metadata: {
68+
expression: create_expression_metadata()
69+
}
6770
});
6871

6972
parser.allow_whitespace();
@@ -244,7 +247,10 @@ function open(parser) {
244247
error: null,
245248
pending: null,
246249
then: null,
247-
catch: null
250+
catch: null,
251+
metadata: {
252+
expression: create_expression_metadata()
253+
}
248254
});
249255

250256
if (parser.eat('then')) {
@@ -326,7 +332,10 @@ function open(parser) {
326332
start,
327333
end: -1,
328334
expression,
329-
fragment: create_fragment()
335+
fragment: create_fragment(),
336+
metadata: {
337+
expression: create_expression_metadata()
338+
}
330339
});
331340

332341
parser.stack.push(block);
@@ -461,7 +470,10 @@ function next(parser) {
461470
elseif: true,
462471
test: expression,
463472
consequent: create_fragment(),
464-
alternate: null
473+
alternate: null,
474+
metadata: {
475+
expression: create_expression_metadata()
476+
}
465477
});
466478

467479
parser.stack.push(child);
@@ -624,7 +636,10 @@ function special(parser) {
624636
type: 'HtmlTag',
625637
start,
626638
end: parser.index,
627-
expression
639+
expression,
640+
metadata: {
641+
expression: create_expression_metadata()
642+
}
628643
});
629644

630645
return;
@@ -699,6 +714,9 @@ function special(parser) {
699714
declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }],
700715
start: start + 2, // start at const, not at @const
701716
end: parser.index - 1
717+
},
718+
metadata: {
719+
expression: create_expression_metadata()
702720
}
703721
});
704722
}
@@ -725,6 +743,7 @@ function special(parser) {
725743
end: parser.index,
726744
expression: /** @type {AST.RenderTag['expression']} */ (expression),
727745
metadata: {
746+
expression: create_expression_metadata(),
728747
dynamic: false,
729748
arguments: [],
730749
path: [],

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,9 @@ export function AssignmentExpression(node, context) {
2323
}
2424
}
2525

26+
if (context.state.expression) {
27+
context.state.expression.has_assignment = true;
28+
}
29+
2630
context.next();
2731
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,8 @@ export function AwaitBlock(node, context) {
4141

4242
mark_subtree_dynamic(context.path);
4343

44-
context.next();
44+
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
45+
if (node.pending) context.visit(node.pending);
46+
if (node.then) context.visit(node.then);
47+
if (node.catch) context.visit(node.catch);
4548
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,8 @@ export function ConstTag(node, context) {
3232
e.const_tag_invalid_placement(node);
3333
}
3434

35-
context.next();
35+
const declaration = node.declaration.declarations[0];
36+
37+
context.visit(declaration.id);
38+
context.visit(declaration.init, { ...context.state, expression: node.metadata.expression });
3639
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ export function HtmlTag(node, context) {
1515
// unfortunately this is necessary in order to fix invalid HTML
1616
mark_subtree_dynamic(context.path);
1717

18-
context.next();
18+
context.next({ ...context.state, expression: node.metadata.expression });
1919
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export function Identifier(node, context) {
9090
if (binding) {
9191
if (context.state.expression) {
9292
context.state.expression.dependencies.add(binding);
93+
context.state.expression.references.add(binding);
9394
context.state.expression.has_state ||=
9495
binding.kind !== 'static' &&
9596
!binding.is_function() &&

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,11 @@ export function IfBlock(node, context) {
1717

1818
mark_subtree_dynamic(context.path);
1919

20-
context.next();
20+
context.visit(node.test, {
21+
...context.state,
22+
expression: node.metadata.expression
23+
});
24+
25+
context.visit(node.consequent);
26+
if (node.alternate) context.visit(node.alternate);
2127
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ export function KeyBlock(node, context) {
1616

1717
mark_subtree_dynamic(context.path);
1818

19-
context.next();
19+
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
20+
context.visit(node.fragment);
2021
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ export function MemberExpression(node, context) {
1515
}
1616
}
1717

18-
if (context.state.expression && !is_pure(node, context)) {
19-
context.state.expression.has_state = true;
18+
if (context.state.expression) {
19+
context.state.expression.has_member_expression = true;
20+
context.state.expression.has_state ||= !is_pure(node, context);
2021
}
2122

2223
if (!is_safe_identifier(node, context.state.scope)) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function RenderTag(node, context) {
5454

5555
mark_subtree_dynamic(context.path);
5656

57-
context.visit(callee);
57+
context.visit(callee, { ...context.state, expression: node.metadata.expression });
5858

5959
for (const arg of expression.arguments) {
6060
const metadata = create_expression_metadata();

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,9 @@ export function UpdateExpression(node, context) {
2121
}
2222
}
2323

24+
if (context.state.expression) {
25+
context.state.expression.has_assignment = true;
26+
}
27+
2428
context.next();
2529
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ export function visit_function(node, context) {
1313
scope: context.state.scope
1414
};
1515

16+
if (context.state.expression) {
17+
for (const [name] of context.state.scope.references) {
18+
const binding = context.state.scope.get(name);
19+
20+
if (binding && binding.scope.function_depth < context.state.scope.function_depth) {
21+
context.state.expression.references.add(binding);
22+
}
23+
}
24+
}
25+
1626
context.next({
1727
...context.state,
1828
function_depth: context.state.function_depth + 1,
Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
1-
/** @import { Expression } from 'estree' */
21
/** @import { AST } from '#compiler' */
32
/** @import { ComponentContext } from '../types' */
43
import * as b from '../../../../utils/builders.js';
4+
import { build_expression } from './shared/utils.js';
55

66
/**
77
* @param {AST.AttachTag} node
88
* @param {ComponentContext} context
99
*/
1010
export function AttachTag(node, context) {
11-
context.state.init.push(
12-
b.stmt(
13-
b.call(
14-
'$.attach',
15-
context.state.node,
16-
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
17-
)
18-
)
19-
);
11+
const expression = build_expression(context, node.expression, node.metadata.expression);
12+
context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression))));
2013
context.next();
2114
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
1+
/** @import { BlockStatement, Pattern, Statement } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
44
import { extract_identifiers } from '../../../../utils/ast.js';
55
import * as b from '#compiler/builders';
66
import { create_derived } from '../utils.js';
77
import { get_value } from './shared/declarations.js';
8+
import { build_expression } from './shared/utils.js';
89

910
/**
1011
* @param {AST.AwaitBlock} node
@@ -14,7 +15,7 @@ export function AwaitBlock(node, context) {
1415
context.state.template.push_comment();
1516

1617
// Visit {#await <expression>} first to ensure that scopes are in the correct order
17-
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
18+
const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression));
1819

1920
let then_block;
2021
let catch_block;

packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
/** @import { Expression, Pattern } from 'estree' */
1+
/** @import { Pattern } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import { dev } from '../../../../state.js';
55
import { extract_identifiers } from '../../../../utils/ast.js';
66
import * as b from '#compiler/builders';
77
import { create_derived } from '../utils.js';
88
import { get_value } from './shared/declarations.js';
9+
import { build_expression } from './shared/utils.js';
910

1011
/**
1112
* @param {AST.ConstTag} node
@@ -15,15 +16,8 @@ export function ConstTag(node, context) {
1516
const declaration = node.declaration.declarations[0];
1617
// TODO we can almost certainly share some code with $derived(...)
1718
if (declaration.id.type === 'Identifier') {
18-
context.state.init.push(
19-
b.const(
20-
declaration.id,
21-
create_derived(
22-
context.state,
23-
b.thunk(/** @type {Expression} */ (context.visit(declaration.init)))
24-
)
25-
)
26-
);
19+
const init = build_expression(context, declaration.init, node.metadata.expression);
20+
context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init))));
2721

2822
context.state.transform[declaration.id.name] = { read: get_value };
2923

@@ -48,13 +42,15 @@ export function ConstTag(node, context) {
4842

4943
// TODO optimise the simple `{ x } = y` case — we can just return `y`
5044
// instead of destructuring it only to return a new object
45+
const init = build_expression(
46+
{ ...context, state: child_state },
47+
declaration.init,
48+
node.metadata.expression
49+
);
5150
const fn = b.arrow(
5251
[],
5352
b.block([
54-
b.const(
55-
/** @type {Pattern} */ (context.visit(declaration.id, child_state)),
56-
/** @type {Expression} */ (context.visit(declaration.init, child_state))
57-
),
53+
b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
5854
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
5955
])
6056
);

packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { BlockStatement, Expression, Identifier, Pattern, SequenceExpression, Statement } from 'estree' */
1+
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
22
/** @import { AST, Binding } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
/** @import { Scope } from '../../../scope' */
@@ -12,8 +12,8 @@ import {
1212
import { dev } from '../../../../state.js';
1313
import { extract_paths, object } from '../../../../utils/ast.js';
1414
import * as b from '#compiler/builders';
15-
import { build_getter } from '../utils.js';
1615
import { get_value } from './shared/declarations.js';
16+
import { build_expression } from './shared/utils.js';
1717

1818
/**
1919
* @param {AST.EachBlock} node
@@ -24,11 +24,18 @@ export function EachBlock(node, context) {
2424

2525
// expression should be evaluated in the parent scope, not the scope
2626
// created by the each block itself
27-
const collection = /** @type {Expression} */ (
28-
context.visit(node.expression, {
29-
...context.state,
30-
scope: /** @type {Scope} */ (context.state.scope.parent)
31-
})
27+
const parent_scope_state = {
28+
...context.state,
29+
scope: /** @type {Scope} */ (context.state.scope.parent)
30+
};
31+
32+
const collection = build_expression(
33+
{
34+
...context,
35+
state: parent_scope_state
36+
},
37+
node.expression,
38+
node.metadata.expression
3239
);
3340

3441
if (!each_node_meta.is_controlled) {

packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
/** @import { Expression } from 'estree' */
21
/** @import { AST } from '#compiler' */
32
/** @import { ComponentContext } from '../types' */
43
import { is_ignored } from '../../../../state.js';
54
import * as b from '#compiler/builders';
5+
import { build_expression } from './shared/utils.js';
66

77
/**
88
* @param {AST.HtmlTag} node
@@ -11,7 +11,7 @@ import * as b from '#compiler/builders';
1111
export function HtmlTag(node, context) {
1212
context.state.template.push_comment();
1313

14-
const expression = /** @type {Expression} */ (context.visit(node.expression));
14+
const expression = build_expression(context, node.expression, node.metadata.expression);
1515

1616
const is_svg = context.state.metadata.namespace === 'svg';
1717
const is_mathml = context.state.metadata.namespace === 'mathml';

packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '#compiler/builders';
5+
import { build_expression } from './shared/utils.js';
56

67
/**
78
* @param {AST.IfBlock} node
@@ -31,14 +32,16 @@ export function IfBlock(node, context) {
3132
statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate)));
3233
}
3334

35+
const test = build_expression(context, node.test, node.metadata.expression);
36+
3437
/** @type {Expression[]} */
3538
const args = [
3639
node.elseif ? b.id('$$anchor') : context.state.node,
3740
b.arrow(
3841
[b.id('$$render')],
3942
b.block([
4043
b.if(
41-
/** @type {Expression} */ (context.visit(node.test)),
44+
test,
4245
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
4346
alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
4447
)

packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '#compiler/builders';
5+
import { build_expression } from './shared/utils.js';
56

67
/**
78
* @param {AST.KeyBlock} node
@@ -10,7 +11,7 @@ import * as b from '#compiler/builders';
1011
export function KeyBlock(node, context) {
1112
context.state.template.push_comment();
1213

13-
const key = /** @type {Expression} */ (context.visit(node.expression));
14+
const key = build_expression(context, node.expression, node.metadata.expression);
1415
const body = /** @type {Expression} */ (context.visit(node.fragment));
1516

1617
context.state.init.push(

0 commit comments

Comments
 (0)