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!}