Skip to content

Commit 96e088c

Browse files
committed
base fix
1 parent d3639b7 commit 96e088c

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