Skip to content

Commit 0071e02

Browse files
fix: allow ts casts in bindings (#10181)
fixes #10179 --------- Co-authored-by: Simon Holthausen <[email protected]>
1 parent 2861ad6 commit 0071e02

File tree

9 files changed

+55
-38
lines changed

9 files changed

+55
-38
lines changed

.changeset/selfish-dragons-knock.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: allow ts casts in bindings

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,17 +338,19 @@ export const validation = {
338338
BindDirective(node, context) {
339339
validate_no_const_assignment(node, node.expression, context.state.scope, true);
340340

341-
let left = node.expression;
341+
const assignee = unwrap_ts_expression(node.expression);
342+
let left = assignee;
343+
342344
while (left.type === 'MemberExpression') {
343-
left = /** @type {import('estree').MemberExpression} */ (left.object);
345+
left = unwrap_ts_expression(/** @type {import('estree').MemberExpression} */ (left.object));
344346
}
345347

346348
if (left.type !== 'Identifier') {
347349
error(node, 'invalid-binding-expression');
348350
}
349351

350352
if (
351-
node.expression.type === 'Identifier' &&
353+
assignee.type === 'Identifier' &&
352354
node.name !== 'this' // bind:this also works for regular variables
353355
) {
354356
const binding = context.state.scope.get(left.name);

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as b from '../../../utils/builders.js';
2-
import { extract_paths, is_simple_expression } from '../../../utils/ast.js';
2+
import { extract_paths, is_simple_expression, unwrap_ts_expression } from '../../../utils/ast.js';
33
import { error } from '../../../errors.js';
44
import {
55
PROPS_IS_LAZY_INITIAL,
@@ -223,10 +223,11 @@ function is_expression_async(expression) {
223223
export function serialize_set_binding(node, context, fallback, options) {
224224
const { state, visit } = context;
225225

226+
const assignee = unwrap_ts_expression(node.left);
226227
if (
227-
node.left.type === 'ArrayPattern' ||
228-
node.left.type === 'ObjectPattern' ||
229-
node.left.type === 'RestElement'
228+
assignee.type === 'ArrayPattern' ||
229+
assignee.type === 'ObjectPattern' ||
230+
assignee.type === 'RestElement'
230231
) {
231232
// Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code
232233
const tmp_id = context.state.scope.generate('tmp');
@@ -237,7 +238,7 @@ export function serialize_set_binding(node, context, fallback, options) {
237238
/** @type {import('estree').Expression[]} */
238239
const assignments = [];
239240

240-
const paths = extract_paths(node.left);
241+
const paths = extract_paths(assignee);
241242

242243
for (const path of paths) {
243244
const value = path.expression?.(b.id(tmp_id));
@@ -275,11 +276,11 @@ export function serialize_set_binding(node, context, fallback, options) {
275276
}
276277
}
277278

278-
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
279-
error(node, 'INTERNAL', `Unexpected assignment type ${node.left.type}`);
279+
if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') {
280+
error(node, 'INTERNAL', `Unexpected assignment type ${assignee.type}`);
280281
}
281282

282-
let left = node.left;
283+
let left = assignee;
283284

284285
// Handle class private/public state assignment cases
285286
while (left.type === 'MemberExpression') {
@@ -342,7 +343,7 @@ export function serialize_set_binding(node, context, fallback, options) {
342343
}
343344
}
344345
// @ts-expect-error
345-
left = left.object;
346+
left = unwrap_ts_expression(left.object);
346347
}
347348

348349
if (left.type !== 'Identifier') {

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

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
extract_paths,
44
is_event_attribute,
55
is_text_attribute,
6-
object
6+
object,
7+
unwrap_ts_expression
78
} from '../../../../utils/ast.js';
89
import { binding_properties } from '../../../bindings.js';
910
import {
@@ -2579,24 +2580,9 @@ export const template_visitors = {
25792580
},
25802581
BindDirective(node, context) {
25812582
const { state, path, visit } = context;
2582-
2583-
/** @type {import('estree').Expression[]} */
2584-
const properties = [];
2585-
2586-
let expression = node.expression;
2587-
while (expression.type === 'MemberExpression') {
2588-
properties.unshift(
2589-
expression.computed
2590-
? /** @type {import('estree').Expression} */ (expression.property)
2591-
: b.literal(/** @type {import('estree').Identifier} */ (expression.property).name)
2592-
);
2593-
expression = /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (
2594-
expression.object
2595-
);
2596-
}
2597-
2598-
const getter = b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression)));
2599-
const assignment = b.assignment('=', node.expression, b.id('$$value'));
2583+
const expression = unwrap_ts_expression(node.expression);
2584+
const getter = b.thunk(/** @type {import('estree').Expression} */ (visit(expression)));
2585+
const assignment = b.assignment('=', expression, b.id('$$value'));
26002586
const setter = b.arrow(
26012587
[b.id('$$value')],
26022588
serialize_set_binding(
@@ -2716,7 +2702,7 @@ export const template_visitors = {
27162702
setter,
27172703
/** @type {import('estree').Expression} */ (
27182704
// if expression is not an identifier, we know it can't be a signal
2719-
node.expression.type === 'Identifier' ? node.expression : undefined
2705+
expression.type === 'Identifier' ? expression : undefined
27202706
)
27212707
);
27222708
break;
@@ -2765,7 +2751,7 @@ export const template_visitors = {
27652751
group_getter = b.thunk(
27662752
b.block([
27672753
b.stmt(serialize_attribute_value(value, context)[1]),
2768-
b.return(/** @type {import('estree').Expression} */ (visit(node.expression)))
2754+
b.return(/** @type {import('estree').Expression} */ (visit(expression)))
27692755
])
27702756
);
27712757
}

packages/svelte/src/compiler/utils/ast.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { error } from '../errors.js';
21
import * as b from '../utils/builders.js';
32

43
/**
@@ -7,10 +6,13 @@ import * as b from '../utils/builders.js';
76
* @returns {import('estree').Identifier | null}
87
*/
98
export function object(expression) {
9+
expression = unwrap_ts_expression(expression);
10+
1011
while (expression.type === 'MemberExpression') {
1112
expression = /** @type {import('estree').MemberExpression | import('estree').Identifier} */ (
1213
expression.object
1314
);
15+
expression = unwrap_ts_expression(expression);
1416
}
1517

1618
if (expression.type !== 'Identifier') {
@@ -270,6 +272,9 @@ function _extract_paths(assignments = [], param, expression, update_expression)
270272
* The Acorn TS plugin defines `foo!` as a `TSNonNullExpression` node, and
271273
* `foo as Bar` as a `TSAsExpression` node. This function unwraps those.
272274
*
275+
* We can't just remove the typescript AST nodes in the parser stage because subsequent
276+
* parsing would fail, since AST start/end nodes would point at the wrong positions.
277+
*
273278
* @template {import('#compiler').SvelteNode | undefined | null} T
274279
* @param {T} node
275280
* @returns {T}
@@ -279,8 +284,14 @@ export function unwrap_ts_expression(node) {
279284
return node;
280285
}
281286

282-
// @ts-expect-error these types don't exist on the base estree types
283-
if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') {
287+
if (
288+
// @ts-expect-error these types don't exist on the base estree types
289+
node.type === 'TSNonNullExpression' ||
290+
// @ts-expect-error these types don't exist on the base estree types
291+
node.type === 'TSAsExpression' ||
292+
// @ts-expect-error these types don't exist on the base estree types
293+
node.type === 'TSSatisfiesExpression'
294+
) {
284295
// @ts-expect-error
285296
return node.expression;
286297
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { test } from '../../test';
22

33
export default test({
4-
html: '1 2'
4+
html: '1 2 <div></div> <input type="number"> <input type="number">',
5+
ssrHtml: '1 2 <div></div> <input type="number" value="1"> <input type="number" value="2">'
56
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
<script lang="ts">
22
let count = $state(1) as number;
33
let double = $derived(count as number * 2) as number;
4+
5+
let element = null;
6+
let with_state = $state({ foo: 1 });
7+
let without_state = { foo: 2 };
48
</script>
59

610
{count as number} {double as number}
11+
12+
<div bind:this={element as HTMLElement}></div>
13+
<input type="number" bind:value={(with_state as { foo: number }).foo} />
14+
<input type="number" bind:value={(without_state as { foo: number }).foo as number} />
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { test } from '../../test';
22

33
export default test({
4-
html: '1 2'
4+
html: '1 2 <input type="number">'
55
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<script lang="ts">
22
let count = $state(1)!;
33
let double = $derived(count! * 2)!;
4+
let binding = $state(null);
45
</script>
56

67
{count!} {double!}
8+
9+
<input type="number" bind:value={binding!} />

0 commit comments

Comments
 (0)