diff --git a/.changeset/chubby-wasps-marry.md b/.changeset/chubby-wasps-marry.md index e92f4c44c..0362f25b5 100644 --- a/.changeset/chubby-wasps-marry.md +++ b/.changeset/chubby-wasps-marry.md @@ -1,6 +1,6 @@ --- -"svelte-check": patch -"svelte2tsx": patch +'svelte-check': patch +'svelte2tsx': patch --- fix: properly handle props with the name `slot` inside Svelte 5 snippets diff --git a/.changeset/metal-hotels-film.md b/.changeset/metal-hotels-film.md new file mode 100644 index 000000000..918957b4d --- /dev/null +++ b/.changeset/metal-hotels-film.md @@ -0,0 +1,6 @@ +--- +'svelte-check': minor +'svelte2tsx': patch +--- + +feat: support Svelte 5 declaration tags diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/expectedv2.json new file mode 100644 index 000000000..3aab95978 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/expectedv2.json @@ -0,0 +1,46 @@ +[ + { + "range": { + "start": { "line": 11, "character": 9 }, + "end": { "line": 11, "character": 14 } + }, + "severity": 1, + "source": "ts", + "message": "Type 'string' is not assignable to type 'number'.", + "code": 2322, + "tags": [] + }, + { + "range": { + "start": { "line": 12, "character": 11 }, + "end": { "line": 12, "character": 18 } + }, + "severity": 1, + "source": "ts", + "message": "Type 'number' is not assignable to type 'string'.", + "code": 2322, + "tags": [] + }, + { + "range": { + "start": { "line": 14, "character": 8 }, + "end": { "line": 14, "character": 21 } + }, + "severity": 1, + "source": "ts", + "message": "This comparison appears to be unintentional because the types 'string' and 'number' have no overlap.", + "code": 2367, + "tags": [] + }, + { + "range": { + "start": { "line": 14, "character": 24 }, + "end": { "line": 14, "character": 41 } + }, + "severity": 1, + "source": "ts", + "message": "This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.", + "code": 2367, + "tags": [] + } +] diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/input.svelte new file mode 100644 index 000000000..01db9ebea --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/input.svelte @@ -0,0 +1,20 @@ + + +{#each boxes as box} + {const area = box.width * box.height} + {let label: number = $state(`${area} square pixels`)} + {const doubled: string = area * 2} + +

{doubled === 1} {label === 'large'}

+
+ {const area = 'nested'} + {area} +
+{/each} diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts index 8fbda3710..e93b98217 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts @@ -2,7 +2,14 @@ import MagicString from 'magic-string'; import { walk } from 'estree-walker'; // @ts-ignore import { TemplateNode, Text } from 'svelte/types/compiler/interfaces'; -import { Attribute, BaseNode, BaseDirective, StyleDirective, ConstTag } from '../interfaces'; +import { + Attribute, + BaseNode, + BaseDirective, + StyleDirective, + ConstTag, + DeclarationTag +} from '../interfaces'; import { parseHtmlx } from '../utils/htmlxparser'; import { handleActionDirective } from './nodes/Action'; import { handleAnimateDirective } from './nodes/Animation'; @@ -16,6 +23,7 @@ import { handleTrailingEndComment } from './nodes/Comment'; import { handleConstTag } from './nodes/ConstTag'; +import { handleDeclarationTag } from './nodes/DeclarationTag'; import { handleDebug } from './nodes/DebugTag'; import { handleEach } from './nodes/EachBlock'; import { Element } from './nodes/Element'; @@ -371,6 +379,9 @@ export function convertHtmlxToJsx( case 'ConstTag': handleConstTag(str, node as ConstTag); break; + case 'DeclarationTag': + handleDeclarationTag(str, node as DeclarationTag); + break; case 'RenderTag': handleRenderTag(str, node); break; diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/DeclarationTag.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/DeclarationTag.ts new file mode 100644 index 000000000..9536b0531 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/DeclarationTag.ts @@ -0,0 +1,10 @@ +import MagicString from 'magic-string'; +import { DeclarationTag } from '../../interfaces'; + +/** + * `{let x = y}` --> `let x = y;` + */ +export function handleDeclarationTag(str: MagicString, declarationTag: DeclarationTag): void { + str.remove(declarationTag.start, declarationTag.declaration.start); + str.overwrite(declarationTag.declaration.end, declarationTag.end, ';'); +} diff --git a/packages/svelte2tsx/src/interfaces.ts b/packages/svelte2tsx/src/interfaces.ts index ebdfe4d93..d64a77e8d 100644 --- a/packages/svelte2tsx/src/interfaces.ts +++ b/packages/svelte2tsx/src/interfaces.ts @@ -1,4 +1,4 @@ -import { ArrayPattern, Identifier, ObjectPattern, Node } from 'estree'; +import { ArrayPattern, Identifier, ObjectPattern, Node, VariableDeclaration } from 'estree'; // @ts-ignore import { DirectiveType, TemplateNode } from 'svelte/types/compiler/interfaces'; @@ -23,6 +23,11 @@ export interface ConstTag extends NodeRange { expression: any; } +export interface DeclarationTag extends NodeRange { + type: 'DeclarationTag'; + declaration: VariableDeclaration & NodeRange & { kind: 'let' | 'const' }; +} + // Copied from the Svelte type definitions export interface BaseNode { start: number; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/expectedv2.ts new file mode 100644 index 000000000..36a98316d --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/expectedv2.ts @@ -0,0 +1,31 @@ +/// +;function $$render() { + + type Box = { width: number; height: number }; + + let boxes: Box[] = [ + { width: 3, height: 4 }, + { width: 5, height: 7 } + ]; +; +async () => { + + for(let box of __sveltets_2_ensureArray(boxes)){ + const foo/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + const area = await 'snippet'; + area; + };return __sveltets_2_any(0)};const area = box.width * box.height; + let label = $state(await `${area} square pixels`); + + { svelteHTML.createElement("p", {});label; } + { svelteHTML.createElement("div", {}); + const area = await 'nested'; + area; + } + + +}}; +return { props: /** @type {Record} */ ({}), exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +/*Ωignore_startΩ*/type Input__SvelteComponent_ = ReturnType; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/input.svelte new file mode 100644 index 000000000..a29e6b447 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/input.svelte @@ -0,0 +1,24 @@ + + +{#each boxes as box} + {const area = box.width * box.height} + {let label = $state(await `${area} square pixels`)} + +

{label}

+
+ {const area = await 'nested'} + {area} +
+ + {#snippet foo()} + {const area = await 'snippet'} + {area} + {/snippet} +{/each} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/expectedv2.ts new file mode 100644 index 000000000..7545db73e --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/expectedv2.ts @@ -0,0 +1,27 @@ +/// +;function $$render() { + + type Box = { width: number; height: number }; + + let boxes: Box[] = [ + { width: 3, height: 4 }, + { width: 5, height: 7 } + ]; +; +async () => { + + for(let box of __sveltets_2_ensureArray(boxes)){ + const area = box.width * box.height; + let label = $state(`${area} square pixels`); + const doubled = area * 2; + + { svelteHTML.createElement("p", {});doubled; label; } + { svelteHTML.createElement("div", {}); + const area = 'nested'; + area; + } +}}; +return { props: {} as Record, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +/*Ωignore_startΩ*/type Input__SvelteComponent_ = ReturnType; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/input.svelte new file mode 100644 index 000000000..fa5a1729b --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/input.svelte @@ -0,0 +1,20 @@ + + +{#each boxes as box} + {const area = box.width * box.height} + {let label = $state(`${area} square pixels`)} + {const doubled = area * 2} + +

{doubled} {label}

+
+ {const area = 'nested'} + {area} +
+{/each}