Skip to content

Commit aabab26

Browse files
fix: handle ts expressions when dealing with runes (#9681)
* fix: handle ts expressions when dealing with runes related to #9639 * docs, more tests * simplify --------- Co-authored-by: Rich Harris <[email protected]>
1 parent a31b2e1 commit aabab26

File tree

10 files changed

+93
-12
lines changed

10 files changed

+93
-12
lines changed

.changeset/twelve-onions-juggle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: handle ts expressions when dealing with runes

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
extract_paths,
88
is_event_attribute,
99
is_text_attribute,
10-
object
10+
object,
11+
unwrap_ts_expression
1112
} from '../../utils/ast.js';
1213
import * as b from '../../utils/builders.js';
1314
import { ReservedKeywords, Runes, SVGElements } from '../constants.js';
@@ -660,10 +661,11 @@ const runes_scope_js_tweaker = {
660661
/** @type {import('./types').Visitors} */
661662
const runes_scope_tweaker = {
662663
VariableDeclarator(node, { state }) {
663-
if (node.init?.type !== 'CallExpression') return;
664-
if (get_rune(node.init, state.scope) === null) return;
664+
const init = unwrap_ts_expression(node.init);
665+
if (!init || init.type !== 'CallExpression') return;
666+
if (get_rune(init, state.scope) === null) return;
665667

666-
const callee = node.init.callee;
668+
const callee = init.callee;
667669
if (callee.type !== 'Identifier') return;
668670

669671
const name = callee.name;

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { error } from '../../errors.js';
2-
import { extract_identifiers, is_text_attribute } from '../../utils/ast.js';
2+
import {
3+
extract_identifiers,
4+
get_parent,
5+
is_text_attribute,
6+
unwrap_ts_expression
7+
} from '../../utils/ast.js';
38
import { warn } from '../../warnings.js';
49
import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
510
import { binding_properties } from '../bindings.js';
@@ -491,7 +496,7 @@ function validate_call_expression(node, scope, path) {
491496
const rune = get_rune(node, scope);
492497
if (rune === null) return;
493498

494-
const parent = /** @type {import('#compiler').SvelteNode} */ (path.at(-1));
499+
const parent = /** @type {import('#compiler').SvelteNode} */ (get_parent(path, -1));
495500

496501
if (rune === '$props') {
497502
if (parent.type === 'VariableDeclarator') return;
@@ -703,7 +708,7 @@ export const validation_runes = merge(validation, a11y_validators, {
703708
next({ ...state });
704709
},
705710
VariableDeclarator(node, { state }) {
706-
const init = node.init;
711+
const init = unwrap_ts_expression(node.init);
707712
const rune = get_rune(init, state.scope);
708713

709714
if (rune === null) return;

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { is_hoistable_function } from '../../utils.js';
33
import * as b from '../../../../utils/builders.js';
44
import * as assert from '../../../../utils/assert.js';
55
import { create_state_declarators, get_props_method } from '../utils.js';
6+
import { unwrap_ts_expression } from '../../../../utils/ast.js';
67

78
/** @type {import('../types.js').ComponentVisitors} */
89
export const javascript_visitors_runes = {
@@ -133,7 +134,7 @@ export const javascript_visitors_runes = {
133134
const declarations = [];
134135

135136
for (const declarator of node.declarations) {
136-
const init = declarator.init;
137+
const init = unwrap_ts_expression(declarator.init);
137138
const rune = get_rune(init, state.scope);
138139
if (!rune || rune === '$effect.active' || rune === '$effect.root') {
139140
if (init != null && is_hoistable_function(init)) {
@@ -208,7 +209,8 @@ export const javascript_visitors_runes = {
208209
// TODO
209210
continue;
210211
}
211-
const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments;
212+
213+
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
212214
const value =
213215
args.length === 0
214216
? b.id('undefined')

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { walk } from 'zimmerframe';
22
import { set_scope, get_rune } from '../../scope.js';
3-
import { extract_identifiers, extract_paths, is_event_attribute } from '../../../utils/ast.js';
3+
import {
4+
extract_identifiers,
5+
extract_paths,
6+
is_event_attribute,
7+
unwrap_ts_expression
8+
} from '../../../utils/ast.js';
49
import * as b from '../../../utils/builders.js';
510
import is_reference from 'is-reference';
611
import {
@@ -568,7 +573,8 @@ const javascript_visitors_runes = {
568573
const declarations = [];
569574

570575
for (const declarator of node.declarations) {
571-
const rune = get_rune(declarator.init, state.scope);
576+
const init = unwrap_ts_expression(declarator.init);
577+
const rune = get_rune(init, state.scope);
572578
if (!rune || rune === '$effect.active') {
573579
declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator)));
574580
continue;
@@ -579,7 +585,7 @@ const javascript_visitors_runes = {
579585
continue;
580586
}
581587

582-
const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments;
588+
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
583589
const value =
584590
args.length === 0
585591
? b.id('undefined')

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

+39
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,42 @@ function _extract_paths(assignments = [], param, expression, update_expression)
265265

266266
return assignments;
267267
}
268+
269+
/**
270+
* The Acorn TS plugin defines `foo!` as a `TSNonNullExpression` node, and
271+
* `foo as Bar` as a `TSAsExpression` node. This function unwraps those.
272+
*
273+
* @template {import('#compiler').SvelteNode | undefined | null} T
274+
* @param {T} node
275+
* @returns {T}
276+
*/
277+
export function unwrap_ts_expression(node) {
278+
if (!node) {
279+
return node;
280+
}
281+
282+
// @ts-expect-error these types don't exist on the base estree types
283+
if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') {
284+
// @ts-expect-error
285+
return node.expression;
286+
}
287+
288+
return node;
289+
}
290+
291+
/**
292+
* Like `path.at(x)`, but skips over `TSNonNullExpression` and `TSAsExpression` nodes and eases assertions a bit
293+
* by removing the `| undefined` from the resulting type.
294+
*
295+
* @template {import('#compiler').SvelteNode} T
296+
* @param {T[]} path
297+
* @param {number} at
298+
*/
299+
export function get_parent(path, at) {
300+
let node = path.at(at);
301+
// @ts-expect-error
302+
if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') {
303+
return /** @type {T} */ (path.at(at < 0 ? at - 1 : at + 1));
304+
}
305+
return /** @type {T} */ (node);
306+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: '1 2'
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script lang="ts">
2+
let count = $state(1) as number;
3+
let double = $derived(count as number * 2) as number;
4+
</script>
5+
6+
{count as number} {double as number}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: '1 2'
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script lang="ts">
2+
let count = $state(1)!;
3+
let double = $derived(count! * 2)!;
4+
</script>
5+
6+
{count!} {double!}

0 commit comments

Comments
 (0)