Skip to content

Commit 1f51993

Browse files
committed
base fix
1 parent 2342c87 commit 1f51993

File tree

15 files changed

+269
-16
lines changed

15 files changed

+269
-16
lines changed

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

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

67
/**
78
* @param {AST.AttachTag} node
89
* @param {ComponentContext} context
910
*/
1011
export function AttachTag(node, context) {
12+
const expression = context.state.analysis.runes
13+
? /** @type {Expression} */ (context.visit(node.expression))
14+
: build_legacy_expression(node.expression, context);
1115
context.state.init.push(
1216
b.stmt(
1317
b.call(
1418
'$.attach',
1519
context.state.node,
16-
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
20+
b.thunk(expression)
1721
)
1822
)
1923
);

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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_legacy_expression } from './shared/utils.js';
89

910
/**
1011
* @param {AST.AwaitBlock} node
@@ -14,7 +15,11 @@ 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(
19+
context.state.analysis.runes
20+
? /** @type {Expression} */ (context.visit(node.expression))
21+
: build_legacy_expression(node.expression, context)
22+
);
1823

1924
let then_block;
2025
let catch_block;

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ 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_legacy_expression } from './shared/utils.js';
910

1011
/**
1112
* @param {AST.ConstTag} node
@@ -15,12 +16,15 @@ 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') {
19+
const init = context.state.analysis.runes
20+
? /** @type {Expression} */ (context.visit(declaration.init))
21+
: build_legacy_expression(declaration.init, context);
1822
context.state.init.push(
1923
b.const(
2024
declaration.id,
2125
create_derived(
2226
context.state,
23-
b.thunk(/** @type {Expression} */ (context.visit(declaration.init)))
27+
b.thunk(init)
2428
)
2529
)
2630
);
@@ -48,12 +52,15 @@ export function ConstTag(node, context) {
4852

4953
// TODO optimise the simple `{ x } = y` case — we can just return `y`
5054
// instead of destructuring it only to return a new object
55+
const init = context.state.analysis.runes
56+
? /** @type {Expression} */ (context.visit(declaration.init, child_state))
57+
: build_legacy_expression(declaration.init, { ...context, state: child_state });
5158
const fn = b.arrow(
5259
[],
5360
b.block([
5461
b.const(
5562
/** @type {Pattern} */ (context.visit(declaration.id, child_state)),
56-
/** @type {Expression} */ (context.visit(declaration.init, child_state))
63+
init,
5764
),
5865
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
5966
])

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { extract_paths, object } from '../../../../utils/ast.js';
1414
import * as b from '#compiler/builders';
1515
import { build_getter } from '../utils.js';
1616
import { get_value } from './shared/declarations.js';
17+
import { build_legacy_expression } from './shared/utils.js';
1718

1819
/**
1920
* @param {AST.EachBlock} node
@@ -24,12 +25,16 @@ export function EachBlock(node, context) {
2425

2526
// expression should be evaluated in the parent scope, not the scope
2627
// 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-
})
32-
);
28+
const parent_scope_state = {
29+
...context.state,
30+
scope: /** @type {Scope} */ (context.state.scope.parent)
31+
};
32+
const collection = context.state.analysis.runes
33+
? /** @type {Expression} */ (context.visit(node.expression, parent_scope_state))
34+
: build_legacy_expression(node.expression, {
35+
...context,
36+
state: parent_scope_state
37+
});
3338

3439
if (!each_node_meta.is_controlled) {
3540
context.state.template.push_comment();

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/** @import { ComponentContext } from '../types' */
44
import { is_ignored } from '../../../../state.js';
55
import * as b from '#compiler/builders';
6+
import { build_legacy_expression } from './shared/utils.js';
67

78
/**
89
* @param {AST.HtmlTag} node
@@ -11,7 +12,9 @@ import * as b from '#compiler/builders';
1112
export function HtmlTag(node, context) {
1213
context.state.template.push_comment();
1314

14-
const expression = /** @type {Expression} */ (context.visit(node.expression));
15+
const expression = context.state.analysis.runes
16+
? /** @type {Expression} */ (context.visit(node.expression))
17+
: build_legacy_expression(node.expression, context);
1518

1619
const is_svg = context.state.metadata.namespace === 'svg';
1720
const is_mathml = context.state.metadata.namespace === 'mathml';

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

Lines changed: 7 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_legacy_expression } from './shared/utils.js';
56

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

35+
const test = context.state.analysis.runes
36+
? /** @type {Expression} */ (context.visit(node.test))
37+
: build_legacy_expression(node.test, context);
38+
3439
/** @type {Expression[]} */
3540
const args = [
3641
node.elseif ? b.id('$$anchor') : context.state.node,
3742
b.arrow(
3843
[b.id('$$render')],
3944
b.block([
4045
b.if(
41-
/** @type {Expression} */ (context.visit(node.test)),
46+
test,
4247
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
4348
alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
4449
)
4550
])
4651
)
4752
];
4853

54+
4955
if (node.elseif) {
5056
// We treat this...
5157
//

packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.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_legacy_expression } from './shared/utils.js';
56

67
/**
78
* @param {AST.KeyBlock} node
@@ -10,7 +11,9 @@ 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 = context.state.analysis.runes
15+
? /** @type {Expression} */ (context.visit(node.expression))
16+
: build_legacy_expression(node.expression, context);
1417
const body = /** @type {Expression} */ (context.visit(node.fragment));
1518

1619
context.state.init.push(

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/** @import { ComponentContext } from '../types' */
44
import { unwrap_optional } from '../../../../utils/ast.js';
55
import * as b from '#compiler/builders';
6+
import { build_legacy_expression } from './shared/utils.js';
67

78
/**
89
* @param {AST.RenderTag} node
@@ -31,7 +32,9 @@ export function RenderTag(node, context) {
3132
}
3233
}
3334

34-
let snippet_function = /** @type {Expression} */ (context.visit(callee));
35+
let snippet_function = context.state.analysis.runes
36+
? /** @type {Expression} */ (context.visit(callee))
37+
: build_legacy_expression(/** @type {Expression} */(callee), context);
3538

3639
if (node.metadata.dynamic) {
3740
// If we have a chain expression then ensure a nullish snippet function gets turned into an empty one

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */
22
/** @import { AST, ExpressionMetadata } from '#compiler' */
3-
/** @import { ComponentClientTransformState, Context } from '../../types' */
3+
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
44
import { walk } from 'zimmerframe';
55
import { object } from '../../../../../utils/ast.js';
66
import * as b from '#compiler/builders';
77
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
88
import { regex_is_valid_identifier } from '../../../../patterns.js';
99
import is_reference from 'is-reference';
1010
import { dev, is_ignored, locator } from '../../../../../state.js';
11-
import { create_derived } from '../../utils.js';
11+
import { build_getter, create_derived } from '../../utils.js';
1212

1313
/**
1414
* @param {ComponentClientTransformState} state
@@ -360,3 +360,57 @@ export function validate_mutation(node, context, expression) {
360360
loc && b.literal(loc.column)
361361
);
362362
}
363+
364+
/**
365+
* Serializes an expression with reactivity like in Svelte 4
366+
* @param {Expression} expression
367+
* @param {ComponentContext} context
368+
*/
369+
export function build_legacy_expression(expression, context) {
370+
// To recreate Svelte 4 behaviour, we track the dependencies
371+
// the compiler can 'see', but we untrack the effect itself
372+
const serialized_expression = /** @type {Expression} */ (context.visit(expression));
373+
if (expression.type === "Identifier") return serialized_expression;
374+
375+
/** @type {Expression[]} */
376+
const sequence = [];
377+
378+
for (const [name, nodes] of context.state.scope.references) {
379+
const binding = context.state.scope.get(name);
380+
381+
if (binding === null || binding.kind === 'normal' && binding.declaration_kind !== 'import') continue;
382+
383+
let used = false;
384+
for (const { node, path } of nodes) {
385+
const expressionIdx = path.indexOf(expression);
386+
if (expressionIdx < 0) continue;
387+
// in Svelte 4, #if, #each and #await copy context, so assignments
388+
// aren't propagated to the parent block / component root
389+
const track_assignment = !path.find((node, i) =>
390+
i < expressionIdx - 1 && ["IfBlock", "EachBlock", "AwaitBlock"].includes(node.type)
391+
)
392+
if (track_assignment) {
393+
used = true;
394+
break;
395+
}
396+
const assignment = /** @type {AssignmentExpression|undefined} */(path.find((node, i) => i >= expressionIdx && node.type === "AssignmentExpression"));
397+
if (!assignment || assignment.left !== node && !path.includes(assignment.left)) {
398+
used = true;
399+
break;
400+
}
401+
}
402+
if (!used) continue;
403+
404+
let serialized = build_getter(b.id(name), context.state);
405+
406+
// If the binding is a prop, we need to deep read it because it could be fine-grained $state
407+
// from a runes-component, where mutations don't trigger an update on the prop as a whole.
408+
if (name === '$$props' || name === '$$restProps' || binding.kind === 'bindable_prop') {
409+
serialized = b.call('$.deep_read_state', serialized);
410+
}
411+
412+
sequence.push(serialized);
413+
}
414+
415+
return b.sequence([...sequence, b.call('$.untrack', b.thunk(serialized_expression))]);
416+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
test({ assert, target }) {
6+
const button = target.querySelector('button');
7+
8+
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> [0,0,0,0,0,0,0,0,0]`);
9+
flushSync(() => button?.click());
10+
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> [0,0,0,0,0,0,0,0,1]`);
11+
}
12+
});

0 commit comments

Comments
 (0)