diff --git a/.changeset/twelve-onions-juggle.md b/.changeset/twelve-onions-juggle.md
new file mode 100644
index 000000000000..00018fce3885
--- /dev/null
+++ b/.changeset/twelve-onions-juggle.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: handle ts expressions when dealing with runes
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index a278c8927251..40d4978451e4 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -7,7 +7,8 @@ import {
extract_paths,
is_event_attribute,
is_text_attribute,
- object
+ object,
+ unwrap_ts_expression
} from '../../utils/ast.js';
import * as b from '../../utils/builders.js';
import { ReservedKeywords, Runes, SVGElements } from '../constants.js';
@@ -660,10 +661,11 @@ const runes_scope_js_tweaker = {
/** @type {import('./types').Visitors} */
const runes_scope_tweaker = {
VariableDeclarator(node, { state }) {
- if (node.init?.type !== 'CallExpression') return;
- if (get_rune(node.init, state.scope) === null) return;
+ const init = unwrap_ts_expression(node.init);
+ if (!init || init.type !== 'CallExpression') return;
+ if (get_rune(init, state.scope) === null) return;
- const callee = node.init.callee;
+ const callee = init.callee;
if (callee.type !== 'Identifier') return;
const name = callee.name;
diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js
index e871808c2cc0..c044c36023a3 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/validation.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js
@@ -1,5 +1,10 @@
import { error } from '../../errors.js';
-import { extract_identifiers, is_text_attribute } from '../../utils/ast.js';
+import {
+ extract_identifiers,
+ get_parent,
+ is_text_attribute,
+ unwrap_ts_expression
+} from '../../utils/ast.js';
import { warn } from '../../warnings.js';
import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
import { binding_properties } from '../bindings.js';
@@ -491,7 +496,7 @@ function validate_call_expression(node, scope, path) {
const rune = get_rune(node, scope);
if (rune === null) return;
- const parent = /** @type {import('#compiler').SvelteNode} */ (path.at(-1));
+ const parent = /** @type {import('#compiler').SvelteNode} */ (get_parent(path, -1));
if (rune === '$props') {
if (parent.type === 'VariableDeclarator') return;
@@ -703,7 +708,7 @@ export const validation_runes = merge(validation, a11y_validators, {
next({ ...state });
},
VariableDeclarator(node, { state }) {
- const init = node.init;
+ const init = unwrap_ts_expression(node.init);
const rune = get_rune(init, state.scope);
if (rune === null) return;
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js
index f3b0d2b90842..1f027dfe0205 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js
@@ -3,6 +3,7 @@ import { is_hoistable_function } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js';
import { create_state_declarators, get_props_method } from '../utils.js';
+import { unwrap_ts_expression } from '../../../../utils/ast.js';
/** @type {import('../types.js').ComponentVisitors} */
export const javascript_visitors_runes = {
@@ -133,7 +134,7 @@ export const javascript_visitors_runes = {
const declarations = [];
for (const declarator of node.declarations) {
- const init = declarator.init;
+ const init = unwrap_ts_expression(declarator.init);
const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.active' || rune === '$effect.root') {
if (init != null && is_hoistable_function(init)) {
@@ -208,7 +209,8 @@ export const javascript_visitors_runes = {
// TODO
continue;
}
- const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments;
+
+ const args = /** @type {import('estree').CallExpression} */ (init).arguments;
const value =
args.length === 0
? b.id('undefined')
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index a3fff51495a4..cdb5f70069c5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -1,6 +1,11 @@
import { walk } from 'zimmerframe';
import { set_scope, get_rune } from '../../scope.js';
-import { extract_identifiers, extract_paths, is_event_attribute } from '../../../utils/ast.js';
+import {
+ extract_identifiers,
+ extract_paths,
+ is_event_attribute,
+ unwrap_ts_expression
+} from '../../../utils/ast.js';
import * as b from '../../../utils/builders.js';
import is_reference from 'is-reference';
import {
@@ -568,7 +573,8 @@ const javascript_visitors_runes = {
const declarations = [];
for (const declarator of node.declarations) {
- const rune = get_rune(declarator.init, state.scope);
+ const init = unwrap_ts_expression(declarator.init);
+ const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.active') {
declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator)));
continue;
@@ -579,7 +585,7 @@ const javascript_visitors_runes = {
continue;
}
- const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments;
+ const args = /** @type {import('estree').CallExpression} */ (init).arguments;
const value =
args.length === 0
? b.id('undefined')
diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js
index cf8b859bfc66..28a56d7c4a66 100644
--- a/packages/svelte/src/compiler/utils/ast.js
+++ b/packages/svelte/src/compiler/utils/ast.js
@@ -265,3 +265,42 @@ function _extract_paths(assignments = [], param, expression, update_expression)
return assignments;
}
+
+/**
+ * The Acorn TS plugin defines `foo!` as a `TSNonNullExpression` node, and
+ * `foo as Bar` as a `TSAsExpression` node. This function unwraps those.
+ *
+ * @template {import('#compiler').SvelteNode | undefined | null} T
+ * @param {T} node
+ * @returns {T}
+ */
+export function unwrap_ts_expression(node) {
+ if (!node) {
+ return node;
+ }
+
+ // @ts-expect-error these types don't exist on the base estree types
+ if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') {
+ // @ts-expect-error
+ return node.expression;
+ }
+
+ return node;
+}
+
+/**
+ * Like `path.at(x)`, but skips over `TSNonNullExpression` and `TSAsExpression` nodes and eases assertions a bit
+ * by removing the `| undefined` from the resulting type.
+ *
+ * @template {import('#compiler').SvelteNode} T
+ * @param {T[]} path
+ * @param {number} at
+ */
+export function get_parent(path, at) {
+ let node = path.at(at);
+ // @ts-expect-error
+ if (node.type === 'TSNonNullExpression' || node.type === 'TSAsExpression') {
+ return /** @type {T} */ (path.at(at < 0 ? at - 1 : at + 1));
+ }
+ return /** @type {T} */ (node);
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/_config.js
new file mode 100644
index 000000000000..4fd52f2d549f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: '1 2'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/main.svelte
new file mode 100644
index 000000000000..015c2d979da7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/typescript-as-expression/main.svelte
@@ -0,0 +1,6 @@
+
+
+{count as number} {double as number}
diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/_config.js
new file mode 100644
index 000000000000..4fd52f2d549f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: '1 2'
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/main.svelte
new file mode 100644
index 000000000000..e851a57828e8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/typescript-non-null-expression/main.svelte
@@ -0,0 +1,6 @@
+
+
+{count!} {double!}